﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace puzzle
{
    public class RockField
    {
        private Random random;
        private List<RockTile> passable;
        private List<RockTile> border;

        private List<RockTile> targetTiles;
        private RockTile startTile;
        private RockTile endTile;

        private bool[,] passableMat;
        private bool[,] used;
        private bool[,] rockEnd;
        private RockTileType[,] tiles;
        private RockType[,] rocksMat;
        public int Width { get; set; }
        public int Height { get; set; }

        private int minX;
        private int minY;


        private static double holeChance = 1;
        private static double destructibleChance = 90;
        private static double hollowChance = 90;
        private static double minComplexity = 0;
        private static int minTargetDist = 5;
        private static int blockChance = 10;
        private static double mazeness = 0.8;
        

        public RockField(int nbTargets, int additionalRocks)
        {
            int minMoves = 8 + nbTargets * 4;
            int baseSize = 30 + nbTargets * 25;
            int maxLoop = baseSize * 10;
            int maxRunLength = maxLoop / 2;
            int loop = 0;
            int moves = 0;
            int breakmovelog = 0;
            int breakrunlog = 0;
            int breaklog = 0;
            int brokenloops = 0;
            int maxloopnumber = 0;
            int maxlooplog = 0;
            int validitylog = 0;
            int complexitylog = 0;
            int minmoveslog = 0;

            do
            {

                random = new Random();
                passable = new List<RockTile>();
                passable.Add(new RockTile(0, 0));
                border = new List<RockTile>();
                border.Add(new RockTile(1, 0));
                border.Add(new RockTile(-1, 0));
                border.Add(new RockTile(0, 1));
                border.Add(new RockTile(0, -1));

                for (int i = 0; i < baseSize; i++)
                {
                    RockTile tile = getRandomBorder();
                    border.Remove(tile);
                    passable.Add(tile);

                    RockTile newBorderTile;
                    int k = random.Next(4);
                    for (int j = 0; j < 4; j++)
                    {
                        switch (k)
                        {
                            case 0:
                                newBorderTile = new RockTile(tile.X + 1, tile.Y);
                                if (!passable.Contains(newBorderTile) && !border.Contains(newBorderTile)) border.Add(newBorderTile);
                                break;
                            case 1:
                                newBorderTile = new RockTile(tile.X, tile.Y + 1);
                                if (!passable.Contains(newBorderTile) && !border.Contains(newBorderTile)) border.Add(newBorderTile);
                                break;
                            case 2:
                                newBorderTile = new RockTile(tile.X - 1, tile.Y);
                                if (!passable.Contains(newBorderTile) && !border.Contains(newBorderTile)) border.Add(newBorderTile);
                                break;
                            case 3:
                                newBorderTile = new RockTile(tile.X, tile.Y - 1);
                                if (!passable.Contains(newBorderTile) && !border.Contains(newBorderTile)) border.Add(newBorderTile);
                                break;
                        }
                        k = (k + 1) % 4;
                    }
                }

                List<RockTile> tmp = new List<RockTile>();
                foreach (RockTile tile in passable)
                {
                    if (random.NextDouble() * 100 < blockChance)
                    {
                        tmp.Add(tile);
                    }
                }

                foreach (RockTile tile in tmp)
                {
                    passable.Remove(tile);
                    border.Add(tile);
                }

                passableMat = listToMatrix(passable, border);
                rocksMat = new RockType[Width, Height];

                used = new bool[Width, Height];
                rockEnd = new bool[Width, Height];
                tiles = new RockTileType[Width, Height];

                for (int i = 0; i < Width; i++)
                {
                    for (int j = 0; j < Height; j++)
                    {
                        used[i, j] = false;
                        rockEnd[i, j] = false;
                        if (passableMat[i,j]) {
                            if (random.NextDouble() * 100 < hollowChance) tiles[i, j] = RockTileType.Hollow;
                            else tiles[i, j] = RockTileType.Normal;
                        }
                        else tiles[i, j] = RockTileType.Border;
                        rocksMat[i, j] = RockType.None;
                    }
                }

                ////////////////////////////////////////////

                targetTiles = new List<RockTile>();
                List<RockTile> possibleTargets = passable.Where(t => adjacentBorderCount(t.X - minX, t.Y - minY) < 3).ToList();
                for (int i = 0; i < nbTargets; i++)
                {
                    if (possibleTargets.Count > 0)
                    {
                        RockTile target = possibleTargets[random.Next(possibleTargets.Count)];
                        targetTiles.Add(target);
                        possibleTargets.RemoveAll(t => t.dist(target) < minTargetDist);
                    }
                }

                
                foreach (RockTile tile in targetTiles)
                {
                    tiles[tile.X - minX, tile.Y - minY] = RockTileType.Target;
                    rocksMat[tile.X - minX, tile.Y - minY] = RockType.Target;
                    rockEnd[tile.X - minX, tile.Y - minY] = true;
                }

                for (int i = 0; i < additionalRocks; i++)
                {
                    if (possibleTargets.Count > 0)
                    {
                        RockTile addrock = possibleTargets[random.Next(possibleTargets.Count)];
                        possibleTargets.Remove(addrock);
                        rocksMat[addrock.X - minX, addrock.Y - minY] = RockType.Normal;
                        rockEnd[addrock.X - minX, addrock.Y - minY] = true;
                    }
                }

                bool nextTo = false;
                do
                {
                    nextTo = false;
                    endTile = passable[random.Next(passable.Count)];
                    foreach (RockTile tile in targetTiles)
                    {
                        if (endTile.nextTo(tile))
                        {
                            nextTo = true;
                            break;
                        }
                    }
                } while (targetTiles.Contains(endTile) || !nextTo);

                int endX = endTile.X - minX;
                int endY = endTile.Y - minY;
                

                //////////////////////////

                int currentX = endX;
                int currentY = endY;
                List<RockTile> run = new List<RockTile>();
                
                loop = 0;
                moves = 0;
                RockTile move = null;
                int moveX = 0;
                int moveY = 0;
                RockTile rock = null;
                while ((moves < minMoves || !isValidStart() || getComplexity() < minComplexity) && loop < maxLoop)
                {
                    List<RockTile> possibleMoves = getPossibleMoves(currentX, currentY);
                    if (possibleMoves.Count == 0 || run.Count > maxRunLength)
                    {
                        breaklog++;
                        if (run.Count > maxRunLength) breakrunlog++;
                        else breakmovelog++;
                        brokenloops += loop;
                        break;
                    }
                    move = possibleMoves[random.Next(possibleMoves.Count)];
                    moveX = move.X;
                    moveY = move.Y;

                    List<RockTile> rocks = adjacentRocks(currentX, currentY);
                    if (rocks.Count > 0) rock = rocks[random.Next(rocks.Count)];
                    else rock = null;
                    if (rock != null && (moveX == rock.X || moveY == rock.Y) && getPossibleMoves(moveX, moveY).Count > 1)
                    {
                        int rockX = rock.X;
                        int rockY = rock.Y;
                        cleanRun(run);
                        if (rocksMat[rockX, rockY] == RockType.Target)
                        {
                            rocksMat[currentX, currentY] = RockType.Target;
                            rocksMat[rockX, rockY] = RockType.None;
                            if (run.Count > 1) moves++;
                        }
                        else
                        {
                            rocksMat[currentX, currentY] = RockType.Normal;
                            rocksMat[rockX, rockY] = RockType.None;
                        }


                        if (!rockEnd[rockX, rockY] && random.NextDouble() * 100 < destructibleChance) tiles[rockX, rockY] = RockTileType.Destructible;
                        foreach (RockTile tile in run)
                        {
                            used[tile.X, tile.Y] = true;
                            if (tiles[tile.X, tile.Y] == RockTileType.Destructible) tiles[tile.X, tile.Y] = RockTileType.Normal;
                        }
                        run.Clear();
                        used[rockX, rockY] = true;
                        if (tiles[currentX, currentY] != RockTileType.Target) tiles[currentX, currentY] = RockTileType.Normal;
                    }
                    else if (rocks.Count < 2)
                    {
                        int holeX = currentX + (currentX - moveX);
                        int holeY = currentY + (currentY - moveY);

                        if (passableMat[holeX, holeY] && (tiles[holeX, holeY] == RockTileType.Normal || tiles[holeX, holeY] == RockTileType.Hollow) && random.NextDouble() * 100 < holeChance)
                        {
                            rocksMat[currentX, currentY] = RockType.Normal;
                            if (tiles[currentX, currentY] != RockTileType.Target) tiles[currentX, currentY] = RockTileType.Normal;
                            tiles[holeX, holeY] = RockTileType.Hole;
                            passableMat[holeX, holeY] = false;
                            cleanRun(run);
                            foreach (RockTile tile in run)
                            {
                                used[tile.X, tile.Y] = true;
                                if (tiles[tile.X, tile.Y] == RockTileType.Destructible) tiles[tile.X, tile.Y] = RockTileType.Normal;
                            }
                            run.Clear();
                        }
                    }

                    currentX = moveX;
                    currentY = moveY;
                    run.Add(new RockTile(currentX, currentY));


                    loop++;
                }

                startTile = new RockTile(currentX, currentY);
                used[currentX, currentY] = true;
                if (tiles[currentX, currentY] == RockTileType.Destructible) tiles[currentX, currentY] = RockTileType.Normal;

                if (loop == maxLoop)
                {
                    maxlooplog++;
                    maxloopnumber += maxLoop;
                    if (!isValidStart()) validitylog++;
                    if (moves < minMoves) minmoveslog++;
                    if (getComplexity() < minComplexity) complexitylog++;
                }
                
            } while (loop == maxLoop || !isValidStart() || moves < minMoves || getComplexity() < minComplexity);
        }

        private RockTile getRandomBorder(){
            return border[border.Count - (int)(Math.Pow(random.NextDouble(), mazeness) * border.Count) - 1];
        }

        private List<RockTile> adjacentRocks(int x, int y)
        {
            List<RockTile> res = new List<RockTile>();
            if (rocksMat[x + 1, y] != RockType.None) res.Add(new RockTile(x + 1, y));
            if (rocksMat[x - 1, y] != RockType.None) res.Add(new RockTile(x - 1, y));
            if (rocksMat[x, y + 1] != RockType.None) res.Add(new RockTile(x, y + 1)); 
            if (rocksMat[x, y - 1] != RockType.None) res.Add(new RockTile(x, y - 1));

            return res;
        }

        private int adjacentBorderCount(int x, int y)
        {
            int res = 0;
            if (!passableMat[x + 1, y]) res++;
            if (!passableMat[x - 1, y]) res++;
            if (!passableMat[x, y + 1]) res++;
            if (!passableMat[x, y - 1]) res++;
            return res;
        }

        public void cleanRun(List<RockTile> run)
        {
            for (int i = 0; i < run.Count - 1; i++)
            {
                for (int j = run.Count - 1; j > i + 1; j--)
                {
                    if (run[i].nextTo(run[j]))
                    {
                        run.RemoveRange(i + 1, j - i - 1);
                        break;
                    }
                }
            }
        }

        private List<RockTile> getPossibleMoves(int currentX, int currentY)
        {
            List<RockTile> possibleMoves= new List<RockTile>();
            int newTileX = currentX + 1;
            int newTileY = currentY;
            if (passableMat[newTileX, newTileY] && rocksMat[newTileX, newTileY] == RockType.None && adjacentBorderCount(newTileX, newTileY) < 3)
            {
                possibleMoves.Add(new RockTile(newTileX, newTileY));
            }

            newTileX = currentX;
            newTileY = currentY + 1;
            if (passableMat[newTileX, newTileY] && rocksMat[newTileX, newTileY] == RockType.None && adjacentBorderCount(newTileX, newTileY) < 3)
            {
                possibleMoves.Add(new RockTile(newTileX, newTileY));
            }

            newTileX = currentX - 1;
            newTileY = currentY;
            if (passableMat[newTileX, newTileY] && rocksMat[newTileX, newTileY] == RockType.None && adjacentBorderCount(newTileX, newTileY) < 3)
            {
                possibleMoves.Add(new RockTile(newTileX, newTileY));
            }

            newTileX = currentX;
            newTileY = currentY - 1;
            if (passableMat[newTileX, newTileY] && rocksMat[newTileX, newTileY] == RockType.None && adjacentBorderCount(newTileX, newTileY) < 3)
            {
                possibleMoves.Add(new RockTile(newTileX, newTileY));
            } 

            return possibleMoves;
        }

        public bool isValidStart()
        {
            for (int i = 0; i < Width; i++)
            {
                for (int j = 0; j < Height; j++)
                {
                    if (tiles[i, j] == RockTileType.Target && rocksMat[i, j] == RockType.Target) return false;
                }
            }
            return true;
        }

        public double getComplexity()
        {
            int free = 0;
            int unused = 0;
            int destr = 0;
            int hollow = 0;
            int hole = 0;
            int pass = 0;
            for (int i = 0; i < Width; i++)
            {
                for (int j = 0; j < Height; j++)
                {
                    if (passableMat[i, j])
                    {
                        pass++;
                        if (!used[i, j]) unused++;
                        else if (tiles[i, j] == RockTileType.Destructible) destr++;
                        else if (tiles[i, j] == RockTileType.Hollow) hollow++;
                        else if (tiles[i, j] == RockTileType.Hole) hole++;
                        else free++;
                    }
                    
                }
            }
            return (double)(6 * destr + 8 * hole - hollow - 2 * free - unused) / pass;
        }

        public RockTile getStartTile(){
            return startTile;
        }

        private bool[,] listToMatrix(List<RockTile> pass, List<RockTile> bord){
            minX = 0;
            int maxX = 0;
            minY = 0;
            int maxY = 0;

            foreach (RockTile tile in bord)
            {
                if (tile.X < minX) minX = tile.X;
                if (tile.Y < minY) minY = tile.Y;
                if (tile.X > maxX) maxX = tile.X;
                if (tile.Y > maxY) maxY = tile.Y;
            }

            Width = maxX - minX + 1;
            Height = maxY - minY + 1;

            bool[,] res = new bool[Width, Height];
            for (int i = 0; i < Width; i++)
            {
                for (int j = 0; j < Height; j++)
                {
                    res[i, j] = false;
                }
            }

            foreach (RockTile tile in pass)
            {
                res[tile.X - minX, tile.Y - minY] = true;
            }

            return res;
        }

        public bool isPassable(int x, int y)
        {
            return passableMat[x, y];
        }

        public RockType getRock(int x, int y)
        {
            return rocksMat[x, y];
        }

        public bool getUsed(int x, int y)
        {
            return used[x, y];
        }

        public RockTileType getTiles(int x, int y)
        {
            return tiles[x, y];
        }

        
    }

    public enum RockTileType{
        Destructible,
        Hole,
        Target,
        Hollow,
        Border,
        Normal
    }

    public enum RockType
    {
        Normal,
        Target,
        None
    }
}
